#!/usr/bin/env python
# -*- coding: utf8 -*-
'''
Show a ports dependence tree.

Styles:
0 - text only
1 - single line pseudographics (default)
2 - double line pseudographics
3 - single line pseudographics, no descendents indicator
4 - double line pseudographics, no descendents indicator
5 - vt100 pseudographic sequences
default or -1 - use ACS pseudographic sequences if detected, else text only

'''

from subprocess import Popen, PIPE
from optparse import OptionParser
import networkx as nx
import fnmatch
import os
import sys
import cPickle as pickle
import curses

# one of the next two lines is needed for graphical output
#import pylab as P
#import matplotlib.pyplot as P

DEPENDS_PREFIXES = ('FETCH', 'EXTRACT', 'PATCH', 'BUILD', 'LIB', 'RUN') 

def acs_assign (pairs):
    """Convert pairs of chars into a dictionary with first char as key and 
    second char as value."""
    mapping = {}
    for key, val in zip(pairs[::2], pairs[1::2]):
        mapping[key] = val
    return mapping

class Net():
    'Show a network as a tree with back references using pseudo-graphics'

    acs_ok = None
    none =     [u']', u'◆', u'◆', u'◆', u'◆', u'\x1b(0`\x1b(B']
    #parent =  [u'-', u'─', u'═', u'─', u'═', u'\x1b(0q\x1b(B']
    child =    [u'+', u'┌', u'╔', u'┌', u'╔', u'\x1b(0l\x1b(B']
    both =     [u'+', u'┬', u'╦', u'',  u'',  u'\x1b(0w\x1b(B']
    ref =      [u' -> ', u' ─> ', u' ═> ', u' → ', u' ⇒ ', u' \x1b(0q\x1b(B> ']
    now_then = [u'+', u'├', u'╠', u'├', u'╠', u'\x1b(0t\x1b(B']
    then =     [u'|', u'│', u'║', u'│', u'║', u'\x1b(0x\x1b(B']
    now =      [u'`', u'└', u'╚', u'└', u'╚', u'\x1b(0m\x1b(B']
    never =    [u' ', u' ', u' ', u' ', u' ', u' ']

    def init_acs(self):
        '''Initialize table of char sequences for pseudo-graphic glyphs in 
        alternative character set of termcap/terminfo.
        '''
        curses.setupterm()
        en_acs = curses.tigetstr("enacs")
        sm_acs = curses.tigetstr("smacs")
        rm_acs = curses.tigetstr("rmacs")
        acs_c = curses.tigetstr("acsc")
        if not (en_acs and sm_acs and rm_acs):
            self.acs_ok = False
            self.style = 0
            return
        acs_dict = acs_assign(acs_c)
        for ind in "`qlwtx":
            if ind not in acs_dict.keys():
                self.acs_ok = False
                self.style = 0
                return
        if sm_acs is None:
            sm_acs = ''
        if rm_acs is None:
            rm_acs = ''
        self.none.append(sm_acs+acs_dict["`"]+rm_acs)     #'◆'
        #self.parent.append(sm_acs+acs_dict['q']+rm_acs)  #'─'
        self.child.append(sm_acs+acs_dict['l']+rm_acs)    #'┌'
        self.both.append(sm_acs+acs_dict['w']+rm_acs)     #'┬'
        self.ref.append(u" "+sm_acs+acs_dict['q']+rm_acs+u"> ") #' ─> '
        self.now_then.append(sm_acs+acs_dict['t']+rm_acs) #'├'
        self.then.append(sm_acs+acs_dict['x']+rm_acs)     #'│'
        self.now.append(sm_acs+acs_dict['m']+rm_acs)      #'└'
        self.never.append(u' ')
        if en_acs:
            print en_acs,
        self.acs_ok = True

    def __init__(self, net, options):
        self.net = net
        self.line = 0
        self.style = options.style
        self.number_all = options.number_all
        self.shown = {}
        self.portsdir = options.portsdir
        self.ignore = len(self.portsdir) + 1
        if self.acs_ok is None and self.style == -1:
            self.init_acs()

    def name(self, index):
        'Return names on a node determined by index'
        if index.startswith(self.portsdir):
            return str(index[self.ignore:])
        else:
            return str(index)

    def deeper1(self, parent_is_last, has_child):
        'Prepare addition to prefix for deeper tree traversing'
        if parent_is_last is None and has_child:
            return self.child[self.style] # '┌'
        if parent_is_last is None and not has_child:
            return self.none[self.style] # '⊢'
        if parent_is_last and has_child:
            return self.now[self.style]+self.both[self.style] # '└┬'
        if parent_is_last and not has_child:
            return self.now[self.style] # '└'
        if not parent_is_last and has_child:
            return self.now_then[self.style]+self.both[self.style] # '├┬'
        if not parent_is_last and not has_child:
            return self.now_then[self.style] # '├'

    def deeper2(self, parent_is_last):
        'Prepare addition to prefix for deeper tree traversing'
        if parent_is_last is None:
            return u'' # ''
        if parent_is_last:
            return self.never[self.style] # ' '
        if not parent_is_last:
            return self.then[self.style] # '│'

    def dep_type(self, parent, child):
        "A string showing the type of dependeces by the first char of its name"
        if parent == "" or child == "":
            return ""
        return " (%s)" % "".join(sorted(self.net.edge[parent][child].values()))

    def show_descendants(self, index=0, prefix=u"", last=None, parent=""):
        'Print a directed net- or tree-like object as a pseudo-graphical tree.'
        children = sorted(self.net.neighbors(index),
                          cmp=lambda x,y: cmp(len(self.net.neighbors(y)),
                                              len(self.net.neighbors(x))))

##        children = sorted(self.net.neighbors(index),
##                          cmp=lambda x,y: cmp(self.ecc[y], self.ecc[x]))
##        children = sorted(self.net.neighbors(index))
        if index in list(self.shown.keys()):
            result = u''
            if self.number_all:
                result = u'%d' % self.line
                self.line += 1
            result += u'\t%s%s%s%s%s%d' % \
                (prefix, self.deeper1(last, children), self.name(index),
                 self.dep_type(parent, index),
                 self.ref[self.style], self.shown[index]) #' → '
            return result
        result = u'%d\t%s%s%s%s' % \
            (self.line, prefix, self.deeper1(last, children), self.name(index),
             self.dep_type(parent, index))
        self.shown[index] = self.line
        self.line += 1
        new_prefix = prefix + self.deeper2(last)
        if children:
            for child in children[:-1]:
                result = u'\n'.join([result,
                    self.show_descendants(child, new_prefix, False, index)])
            result = u'\n'.join([result,
                self.show_descendants(children[-1], new_prefix, True, index)])
        return result

    def show(self, index=None):
        'Show whole graph'
        self.ecc = {}
        for component in nx.strongly_connected_component_subgraphs(self.net):
            tmp = nx.eccentricity(component)
            if tmp:
                self.ecc.update(tmp)
        if index is not None:
            return self.show_descendants(index)
        else:
            return u'\n'.join([self.show_descendants(ind)
                                for ind in sorted(self.net.nodes_iter())
                                    if ind not in list(self.shown.keys())])

def parse_args():
    'Set a parser up and parse the args.'
    parser = OptionParser()
    parser.add_option("-f", "--fetch-depends", dest="fetch", 
                    action="store_true", help="Use fetch dependences for tree")
    parser.add_option("-e", "--extract-depends", dest="extract",
                   action="store_true", help="Use extract dependences for tree")
    parser.add_option("-p", "--patch-depends", dest="patch",
                    action="store_true", help="Use patch dependences for tree")
    parser.add_option("-b", "--build-depends", dest="build",
                    action="store_true", help="Use build dependences for tree")
    parser.add_option("-l", "--lib-depends", dest="lib",
                    action="store_true", help="Use lib dependences for tree")
    parser.add_option("-r", "--run-depends", dest="run",
                    action="store_true", help="Use run dependences for tree")
    parser.add_option("-n", "--number-all", dest="number_all",
                    action="store_true",
                    help="Number all lines, even with repeated port names")
    parser.add_option("-s", "--style", default=-1, metavar="NUM", type="int",
                      dest="style", help="set style to NUM (-1...5)")
    parser.add_option("-A", "--all-ports", dest="all_ports",
                    action="store_true", help="Use all ports (this is slow!)")
    parser.add_option("-R", "--reverse", dest="reverse",
                    action="store_true", help="Also show reversed tree")
    parser.add_option("-O", "--others", dest="others",
                    action="store_true", help="Show all ports")
    parser.add_option("-P", "--portsdir", dest="portsdir", metavar="DIR",
                    default='/usr/ports',
                    help="Search for ports in directory DIR")
    parser.add_option("-S", "--save", dest="save", metavar="FILE",
                    help="Save the traversed network into FILE")
    parser.add_option("-U", "--use-saved", dest="use_saved", metavar="FILE",
                    help="Use the traversed network saved in FILE")

    return check_args(parser)

def check_args(parser):
    'Check and normalize options and args'
    options, args = parser.parse_args()
    options.all = True not in (options.fetch, options.extract, options.patch,
                options.build, options.lib, options.run)
    if not (-1 <= options.style <= 5):
            parser.error("Style should be an integer in the range -1...5.")
    if not args or args == []:
        args = [os.getcwdu()]
    options.portsdir = os.path.normpath(options.portsdir)
    if not options.portsdir.startswith('/'):
        parser.error("Ports directory should start with a slash '/'.")
    for indx, port in enumerate(args):
        if not port.startswith('/'):
            args[indx] = os.path.join(options.portsdir, port)
    return options, args

def get_depends(port_trees, options):
    "Get all depends needed according to options"
    if options.all:
        all_depends = port_trees["ALL"]
        return all_depends
    all_depend = nx.DiGraph()
    for deptype in DEPENDS_PREFIXES:
        all_depend.add_nodes_from(port_trees[deptype].nodes_iter())
    if options.all or options.fetch:
        all_depend.add_edges_from(port_trees["FETCH"].edges_iter())
    if options.all or options.extract:
        all_depend.add_edges_from(port_trees["EXTRACT"].edges_iter())
    if options.all or options.patch:
        all_depend.add_edges_from(port_trees["PATCH"].edges_iter())
    if options.all or options.build:
        all_depend.add_edges_from(port_trees["BUILD"].edges_iter())
    if options.all or options.lib:
        all_depend.add_edges_from(port_trees["LIB"].edges_iter())
    if options.all or options.run:
        all_depend.add_edges_from(port_trees["RUN"].edges_iter())
    return all_depend

def traverse(port, port_trees):
    "Extend port_tree by port and its descendents"
    for deptype in DEPENDS_PREFIXES:
        port_trees[deptype].add_node(port, visited=False)
    stop = False
    while not stop:
        stop = True
        for tree_ind in port_trees.keys():
            for portind in port_trees[tree_ind].nodes(data=False):
                if port_trees[tree_ind].node[portind]['visited']:
                    continue
                add_one_port(portind, port_trees)
                port_trees[tree_ind].node[portind]['visited'] = True
                stop = False

def add_one_port(port, port_trees):
    "Extend each port_tree by a port and its corresponding children"
    for deptype in DEPENDS_PREFIXES:
        port_trees[deptype].add_node(port, visited=True)
    port_trees["ALL"].add_node(port, visited=True)
    var, err = Popen(('make', '-C', port,
                            '-V', 'FETCH_DEPENDS',
                            '-V', 'EXTRACT_DEPENDS',
                            '-V', 'PATCH_DEPENDS',
                            '-V', 'BUILD_DEPENDS',
                            '-V', 'LIB_DEPENDS',
                            '-V', 'RUN_DEPENDS'),
                        stdout=PIPE, stderr=PIPE).communicate()
    if err:
        print err
        return
    var = var.split('\n')
    for deptype, varind in zip(DEPENDS_PREFIXES, var[:6]):
        if varind in (None, ''):
            continue
        for depend in varind.split():
            dep = depend.split(':')[1]
            if dep not in port_trees[deptype].nodes_iter():
                port_trees[deptype].add_node(dep, visited=False)
            if dep not in port_trees["ALL"].nodes_iter():
                port_trees["ALL"].add_node(dep, visited=False)
            port_trees[deptype].add_edge(port, dep) #repeating an edge is OK
            port_trees["ALL"].add_edge(port, dep, {deptype: deptype[0]})

def print_results(port_tree, options, args):
    "Output the results according to options set by user"
    net = Net(port_tree, options)
    print "Depending on"
    for port in args:
        print net.show(port)
    if options.others:
        print net.show()
    if options.reverse:
        print "\nRequired by:"
        net = Net(port_tree.reverse(), options)
        for port in args:
            print net.show(port)
        if options.others:
            print net.show()

def main():
    "Do the job"
    options, args = parse_args()

    port_trees = {}
    if options.use_saved:
        savefile = open(options.use_saved, "rb")
        port_trees = pickle.load(savefile)
        savefile.close()
    else:
        for deptype in DEPENDS_PREFIXES:
            port_trees[deptype] = nx.DiGraph()
            port_trees["ALL"] = nx.DiGraph()
        if options.all_ports:
            categories = fnmatch.filter(os.listdir(options.portsdir), "[a-z]*")
            for category in sorted(categories):
                if category in ('distfiles', 'packages'):
                    continue
                cat = os.path.join(options.portsdir, category)
                if not os.path.isdir(cat):
                    continue
                print cat
                for port in sorted(os.listdir(cat)):
                    port1 = os.path.join(cat, port)
                    if not os.path.isdir(port1):
                        continue
                    add_one_port(port1, port_trees)
        else:
            for port in args:
                traverse(port, port_trees)

    if options.save:
        savefile = open(options.save, "wb")
        pickle.dump(port_trees, savefile, -1) # save DiDraph, highest protocol
        savefile.close()

    port_tree_all = get_depends(port_trees, options)
    print_results(port_tree_all, options, args)

    # get graphical output with the following lines (plus an import above)
    #nx.draw(port_tree_all)
    #P.draw()
    #P.show()

if __name__ == "__main__":
    main()
